Chapter 5

State Management Basics: setState and Patterns

Session 5

Learning Objectives

By the end of this chapter, you will be able to:

1

Understanding State in Flutter

What is State?

State is any data that can change over time and affects what the user sees. When state changes, the UI needs to rebuild to reflect the new data.

Types of State

  • Ephemeral State: Local to a single widget (e.g., current tab index, text field value, animation progress). Use setState().
  • App State: Shared across multiple widgets or persists across screens (e.g., user authentication, shopping cart, theme preference). Requires state management solutions.

Key principle: Keep state as local as possible. Only lift state up when multiple widgets need to access it.

2

Using setState() for Local State

Basic setState Pattern

setState() tells Flutter that the widget's state has changed and the UI needs to rebuild. It should only be used in StatefulWidget.

Simple Counter Example

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

setState Best Practices

  • Only mutate state variables inside the setState() callback
  • Keep the callback lightweight; do heavy computation before calling setState
  • Don't call setState in build() method
  • Check mounted before setState in async callbacks
3

State Lifting Pattern

When to Lift State

When multiple widgets need to access or modify the same state, lift it to their nearest common ancestor.

Example: Shared Counter

// Parent widget holds the state
class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State {
  int _count = 0;

  void _increment() => setState(() => _count++);
  void _decrement() => setState(() => _count--);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CounterDisplay(count: _count),  // Child reads state
        CounterControls(
          onIncrement: _increment,      // Child modifies state
          onDecrement: _decrement,
        ),
      ],
    );
  }
}

// Child widget receives state as parameter
class CounterDisplay extends StatelessWidget {
  final int count;
  const CounterDisplay({required this.count});

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

// Child widget receives callbacks
class CounterControls extends StatelessWidget {
  final VoidCallback onIncrement;
  final VoidCallback onDecrement;
  
  const CounterControls({
    required this.onIncrement,
    required this.onDecrement,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        ElevatedButton(onPressed: onDecrement, child: Text('-')),
        ElevatedButton(onPressed: onIncrement, child: Text('+')),
      ],
    );
  }
}
4

ValueNotifier and ValueListenableBuilder

Simple Reactive Updates

For simple state that needs to be shared, ValueNotifier provides a lightweight solution without external packages.

ValueNotifier Example

class ThemeNotifier {
  final ValueNotifier isDarkMode = ValueNotifier(false);
  
  void toggleTheme() {
    isDarkMode.value = !isDarkMode.value;
  }
  
  void dispose() {
    isDarkMode.dispose();
  }
}

// Usage in widget
class SettingsScreen extends StatefulWidget {
  @override
  _SettingsScreenState createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State {
  final _themeNotifier = ThemeNotifier();

  @override
  void dispose() {
    _themeNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: _themeNotifier.isDarkMode,
      builder: (context, isDark, child) {
        return Switch(
          value: isDark,
          onChanged: (_) => _themeNotifier.toggleTheme(),
        );
      },
    );
  }
}

When to Use ValueNotifier

  • Simple state that doesn't require complex logic
  • State shared between a few related widgets
  • Prefer over setState when state needs to be accessed from multiple widgets
  • Remember to dispose ValueNotifier to prevent memory leaks
5

Common setState Pitfalls

Pitfall 1: Calling setState in build()

Wrong: Calling setState inside build() causes infinite rebuild loops.

// DON'T DO THIS
@override
Widget build(BuildContext context) {
  setState(() { _count++; });  // ❌ Infinite loop!
  return Text('Count: $_count');
}

Pitfall 2: setState in Async Callbacks

Wrong: Calling setState after widget is disposed causes errors.

// DON'T DO THIS
Future _loadData() async {
  final data = await fetchData();
  setState(() { _items = data; });  // ❌ May be called after dispose
}

Correct: Check if widget is still mounted.

// DO THIS
Future _loadData() async {
  final data = await fetchData();
  if (mounted) {  // ✅ Check before setState
    setState(() { _items = data; });
  }
}

Pitfall 3: Heavy Computation in setState

Wrong: Doing heavy work inside setState blocks the UI.

// DON'T DO THIS
void _processData() {
  setState(() {
    _result = expensiveComputation();  // ❌ Blocks UI
  });
}

Correct: Compute before setState.

// DO THIS
void _processData() {
  final result = expensiveComputation();  // ✅ Compute first
  setState(() {
    _result = result;  // ✅ Just assign
  });
}
6

State Management Decision Guide

When to Use Each Approach

  • setState: Local widget state that doesn't need to be shared (e.g., form field values, toggle states, animation progress)
  • State Lifting: State shared between parent and children widgets (e.g., selected item in a list, form state)
  • ValueNotifier: Simple shared state between a few widgets (e.g., theme preference, simple counters)
  • Provider/State Management Packages: Complex app-wide state, business logic, async operations (covered in Chapter 12)
7

Practical Example: Todo List

Complete Example

class TodoItem {
  final String id;
  final String text;
  bool isCompleted;

  TodoItem({required this.id, required this.text, this.isCompleted = false});
}

class TodoScreen extends StatefulWidget {
  @override
  _TodoScreenState createState() => _TodoScreenState();
}

class _TodoScreenState extends State {
  final List _todos = [];
  final _textController = TextEditingController();

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  void _addTodo(String text) {
    if (text.trim().isEmpty) return;
    setState(() {
      _todos.add(TodoItem(
        id: DateTime.now().toString(),
        text: text,
      ));
    });
    _textController.clear();
  }

  void _toggleTodo(String id) {
    setState(() {
      final todo = _todos.firstWhere((t) => t.id == id);
      todo.isCompleted = !todo.isCompleted;
    });
  }

  void _deleteTodo(String id) {
    setState(() {
      _todos.removeWhere((t) => t.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Todo List')),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _textController,
                    decoration: InputDecoration(hintText: 'Add todo'),
                    onSubmitted: _addTodo,
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () => _addTodo(_textController.text),
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                final todo = _todos[index];
                return ListTile(
                  title: Text(
                    todo.text,
                    style: TextStyle(
                      decoration: todo.isCompleted 
                        ? TextDecoration.lineThrough 
                        : null,
                    ),
                  ),
                  leading: Checkbox(
                    value: todo.isCompleted,
                    onChanged: (_) => _toggleTodo(todo.id),
                  ),
                  trailing: IconButton(
                    icon: Icon(Icons.delete),
                    onPressed: () => _deleteTodo(todo.id),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
8

Exercises

1. Counter with History

Create a counter app that tracks the count history. Display the current count and a list of all previous counts. Add buttons to increment, decrement, and reset. Use setState to manage the state.

2. Temperature Converter

Build a temperature converter that converts between Celsius and Fahrenheit. Use state lifting: parent widget holds the temperature value, child widgets display and modify it. Include input validation.

3. Shopping Cart with ValueNotifier

Create a simple shopping cart using ValueNotifier to manage cart items. Display items, quantities, and total price. Use ValueListenableBuilder to update the UI when cart changes. Include add/remove functionality.